Abraxus's Blog

DiceCTF hyperlink Write Up

Details:

Jeopardy style CTF

Category: Reverse Engineering

Write up:

Looking at the python file I saw:

import json


def test_chain(links, start, end):
    current = start
    for link in links:
        current = int(''.join(
            str(int(current & component != 0))
            for component in link
        ), 2)
    return end == current & end


def main():
    try:
        with open('hyperlink.json', 'r') as f:
            data = json.load(f)
    except IOError:
        print('Could not open hyperlink.json')
        return

    print('Welcome to the chain building game.')
    print('Enter a chain and see if it works:')

    chain = input()

    legal_chars = set('abcdefghijklmnopqrstuvwxyz{}_')
    if any(c not in legal_chars for c in chain):
        print('Chain contains illegal characters!')
        return

    try:
        links = [data['links'][c] for c in chain]
        result = test_chain(links, data['start'], data['target'])
    except Exception:
        print('Something went wrong!')
        return

    if result:
        print('Chain works! Congratulations.')
    else:
        print('Oh no! Chain does not reach the target.')


if __name__ == '__main__':
    main()

My first step for this was to see how the program acted when I entered some text so I decided to enter dice{ since I knew that was how the flag started. I added some code to print out the bits of the start, end, and each current value:

Welcome to the chain building game.
Enter a chain and see if it works:
dice{
start: 0b10000000000000000000000000000000000010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000
cur:    0b1000000000000000000000000000000000010001000100010001000100011001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000
cur:     0b100000000000000000000000000000000010001000100010001000100010101000110010001100100010001000100010001000110010001000100011001000100010001000100010001000100010001000
cur:      0b10000000000000000000000000000000010001000100010001000100010011000100010001010100010001000100010001000100010001000110010001000100010001000100010001000100010001000
cur:       0b1000000000000000000000000000000010001100100010001000100010011000100011001001100011001000110010001000100010001000101010001000100010001000100010001000100010001100
cur:        0b100000000000000000000000000000010001000100010001000100010011000100010001001100010001000101010001000100010001000100110001000100010001000100011001000100010001000
end:                                     0b1000010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001

This shifted some of the 1's to their correct position so I decided to dive into how the bit values get set. The code for this was in:

    for link in links:
        current = int(''.join(
            str(int(current & component != 0))
            for component in link
        ), 2)

If any bit in both current and component were equal to 1 then we would get 1 for that position, otherwise we would get 0 so I decided to see what all the values in the last "link" was for each character:

j: 0b1
d: 0b1
r: 0b11
m: 0b1
y: 0b1
k: 0b1
v: 0b1
}: 0b1
c: 0b1
h: 0b1
u: 0b1
_: 0b1
l: 0b1
n: 0b1
s: 0b1
q: 0b1
e: 0b1
w: 0b1
b: 0b1
i: 0b1
t: 0b1
p: 0b1
g: 0b1
z: 0b1
f: 0b1
{: 0b1
a: 0b1
o: 0b1
x: 0b1

Looking at the values I noticed that the only value that would ever turn the last value into a 1 had to be 'r' and for that to work the second to last bit needed to equal 1 as well:

100 & 1  == 0
110 & 1  == 0
110 & 11 == 1
111 & 1  == 1

I then wrote a script to loop through all of the link values (0-163) and see which characters were different from the rest:

0 
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 ['_']['g']['n']
37 ['n']
38 ['g']
39 ['_']
40 ['b']['r']['e']
41 ['e']
42 ['b']
43 ['r']
44 ['t']['y']['h']
45 ['y']
46 ['t']
47 ['h']
48 ['}']['a']['r']
49 ['r']
50 ['a']
51 ['}']
52 ['t']['y']['r']
53 ['r']
54 ['y']
55 ['t']
56 ['b']['a']['r']
57 ['b']
58 ['r']
59 ['a']
60 ['c']['i']['d']
61 ['d']
62 ['i']
63 ['c']
64 ['a']['e']['n']
65 ['n']
66 ['e']
67 ['a']
68 ['e']['i']['n']
69 ['i']
70 ['n']
71 ['e']
72 ['v']['e']
73 ['e']
74 ['v']
75 ['e']
76 ['c']['e']['i']
77 ['i']
78 ['c']
79 ['e']
80 ['_']['i']['s']
81 ['_']
82 ['i']
83 ['s']
84 ['y']['r']['e']
85 ['e']
86 ['r']
87 ['y']
88 ['_']['a']['r']
89 ['a']
90 ['r']
91 ['_']
92 ['e']['{']
93 ['e']
94 ['{']
95 ['e']
96 ['_']['g']['i']
97 ['g']
98 ['_']
99 ['i']
100 ['_']['a']['l']
101 ['_']
102 ['a']
103 ['l']
104 ['_']['i']['s']
105 ['i']
106 ['s']
107 ['_']
108 ['i']['l']['n']
109 ['l']
110 ['i']
111 ['n']
112 ['a']['g']['l']
113 ['a']
114 ['l']
115 ['g']
116 ['c']['e']['{']
117 ['c']
118 ['e']
119 ['{']
120 ['g']['i']['n']
121 ['i']
122 ['n']
123 ['g']
124 ['_']['l']['s']
125 ['s']
126 ['_']
127 ['l']
128 ['i']['n']['h']
129 ['h']
130 ['i']
131 ['n']
132 ['t']['i']['h']
133 ['t']
134 ['h']
135 ['i']
136 ['_']['i']['l']
137 ['_']
138 ['l']
139 ['i']
140 ['_']['a']['r']
141 ['r']
142 ['_']
143 ['a']
144 ['v']['e']['{']
145 ['{']
146 ['e']
147 ['v']
148 ['g']['e']['l']
149 ['l']
150 ['g']
151 ['e']
152 ['b']['g']['e']
153 ['g']
154 ['e']
155 ['b']
156 ['v']['r']['e']
157 ['v']
158 ['e']
159 ['r']
160 ['a']['r']['e']
161 ['e']
162 ['a']
163 ['r']

Looking through this a bit I saw that each set of 3 characters would shift and set one of the bits that we needed and saw that all possible sets of 3 were:

geb ear ver lge {ev r_a _li thi hing s_l ing ce{ alg lin is_ _al g_i e{e ar_ ery _is ice eve ine nea dic bra ryt ra} yth ebr ng_

Each link we have here started with what the last one ended with, some would be 2 characters while others were just 1:

dic + ice => dice

I then wrote a script that looped through and combined all the values for the flag:

# import json
import json

# main function
def main():

    # open json data
    with open('hyperlink.json', 'r') as f:
        data = json.load(f)

    # all legal chars
    legal_chars = set('abcdefghijklmnopqrstuvwxyz{}_')
    
    # get start and end
    start = data['start']
    end = data['target']
    
    s = []
    st = ""

    # loop through and find all unique values
    for i in range(0, len(data['links']['a'])):
        arr = {}

        # make a dictionary with all values and characters
        for c in legal_chars:
            if  data['links'][c][i] not in arr:
                arr[data['links'][c][i]] = []
            arr[data['links'][c][i]].append(c)

        # find how many characters for each value
        max_val = 0
        for a in arr:
            if len(arr[a]) > max_val:
                max_val = len(arr[a])

        # remove the value with the most amount of characters
        not_unique = None
        for a in arr:
            if len(arr[a]) == max_val:
                not_unique = a
        arr.pop(not_unique, None)

        # create our array
        if len(arr) > 1:
            if len(st) > 0:
                s.append(st)
            st = ""
        else:
            st += ''.join([str(arr[a][0]) for a in arr])
    
    # start of flag
    flag = " d"
    
    # loop through and find which have overlap
    while len(s) > 1 and flag[-1] != '}':
        pos = []
        pos2 = []
        for i in s:
            if i[0] == flag[-1]:
                pos.append(i)
            elif i[:2] == flag[-2:]:
                pos2.append(i)
        if len(pos2) == 1:
            flag = flag[:-2] + pos2[0]
            s.remove(pos2[0])
        else:
            flag = flag[:-1] + pos[0]
            s.remove(pos[0])

    # print flag
    print(flag)
        


if __name__ == '__main__':
    main()

When run I got:

 dice{everything_is_linear_algebra}